﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Net;

namespace PacketRush.Core
{
    /// <summary>
    /// Represents the server in a server/client network.
    /// </summary>
    /// <remarks>
    /// NetServer represents the server in a server/client network, clients connect
    /// to an instance of NetServer using <see cref="PacketRush.Core.NetClient"/> in 
    /// order to send and receive messages.
    /// 
    /// NetServer and NetClient use services that implement <see cref="PacketRush.Core.INetService"/>
    /// in order to communicate to one another.
    /// </remarks>
    /// <example>
    /// The following example shows how to create an instance of NetServer and register services
    /// <code>
    /// NetServer netServer = new NetServer(IPAddress.Parse("127.0.0.1"), 5000);
    /// ServerNetUserService userSvc = new ServerNetUserService();
    /// ServerMessagingService messagingSvc = new ServerMessagingService();
    /// netServer.RegisterService(userSvc);
    /// netServer.RegisterService(messagingSvc);
    /// netServer.Start();    
    /// </code>
    /// Once the NetServer <see cref="PacketRush.Core.NetServer.Start"/> method is called, the
    /// NetServer instance must have its <see cref="PacketRush.Core.NetServer.Update"/> method
    /// called in order to perform its internal networking tasks such as sending and receiving.
    /// <code>
    /// while(true)
    /// {
    ///     netServer.Update();
    /// }
    /// </code>
    /// </example>
    public partial class NetServer : NetBase
    {
        /// <summary>
        /// The networking provider used by NetServer.
        /// </summary>
        private IServerNetworkingProvider _NetProvider;

        /// <summary>
        /// Describes settings for this <c>NetServer</c> instance.
        /// </summary>
        [TypeConverter(typeof(ExpandableObjectConverter))]
        public NetServerSettings Settings { get; private set; }

        private PacketWriter _PacketWriter = new PacketWriter();

        /// <summary>
        /// The end point of this server.
        /// </summary>
        private EndPoint _LocalEndPoint;

        private ClientInfoCollection _Clients;

        /// <summary>
        /// Used to temporarily store a stack of disconnected connections
        /// </summary>
        private List<ClientInfo> _RecentDisconnections;


        /// <summary>
        /// The last time a ping packet was sent to all connections
        /// </summary>
        private long _LastPingTime;

        /// <summary>
        /// The time elapsed since a ping packet has been sent to all connections
        /// </summary>
        private long ElapsedPingTime
        {
            get
            {
                return _Timer.ElapsedMilliseconds - _LastPingTime;
            }
        }

        private NetTimer _Timer;

        /// <summary>
        /// The time in milliseconds that the server has been running.
        /// </summary>
        /// <remarks>The time starts at zero when <see cref="PacketRush.Core.NetServer.Start"/> is called.</remarks>
        [TypeConverter(typeof(ExpandableObjectConverter))]
        public NetTime RunningTime { get; private set; }

        /// <summary>
        /// The time before the last call to <see cref="PacketRush.Core.NetServer.Update"/> occured in milliseconds.
        /// </summary>
        /// <remarks>This property is actually updated inside <c>Update</c> right before any logic occurs.</remarks>
        private long _BeforeUpdateTime;

        /// <summary>
        /// The time after the last call to <see cref="PacketRush.Core.NetServer.Update"/> occured in milliseconds.
        /// </summary>
        /// <remarks>This property is actually updated inside <c>Update</c> right after all update logic.</remarks>
        private long _AfterUpdateTime;

        /// <summary>
        /// The total time of the last <see cref="PacketRush.Core.NetServer.Update"/> in milliseconds.
        /// </summary>
        /// <remarks>Measure of the time in milliseconds of the time taken to run an <c>Update</c>.</remarks>
        public long UpdateTime
        {
            get
            {
                return _AfterUpdateTime - _BeforeUpdateTime;
            }
        }

        /// <summary>
        /// Gets the number of current connections
        /// </summary>
        public int ConnectionCount
        {
            get
            {
                return _Clients.Count;
            }
        }

        /// <summary>
        /// Create an instance of NetServer.
        /// </summary>
        /// <param name="address">The IP address of the server.</param>
        /// <param name="port">The server port.</param>
        public NetServer(IPAddress address, int port)
            : this(new DefaultServerNetworkingProvider(), new NetServerSettings(), address, port)
        {
        }

        /// <summary>
        /// Create an instance of NetServer.
        /// </summary>
        /// <param name="settings">Settings to initialize this instance with.</param>
        /// <param name="address">The IP address of the server.</param>
        /// <param name="port">The server port.</param>
        public NetServer(NetServerSettings settings, IPAddress address, int port)
            : this(new DefaultServerNetworkingProvider(), settings, address, port)
        {
        }

        /// <summary>
        /// Create an instance of NetServer.
        /// </summary>
        /// <param name="networkingProvider">The networking provider to use for this server.</param>
        /// <param name="address">The IP address of the server.</param>
        /// <param name="port">The server port.</param>
        public NetServer(IServerNetworkingProvider networkingProvider, IPAddress address, int port)
            : this(networkingProvider, new NetServerSettings(), address, port)
        {
        }

        /// <summary>
        /// Create an instance of NetServer specifying the networking provider to use
        /// </summary>
        /// <remarks>
        /// When creating an instance of NetServer, an instance of a type that implements <see cref="PacketRush.Core.IServerNetworkingProvider"/>
        /// can be given. The NetServer uses a networking provider to perform its send, receive and connection logic,
        /// this allows different implementations for different scenarios, for instance, its possible to use
        /// a networking provider that provides all connectivity and communication logic using only UDP.
        /// </remarks>
        /// <param name="networkingProvider">The networking provider to use for this server.</param>
        /// <param name="settings">Settings to initialize this instance with.</param>
        /// <param name="address">The IP address of the server.</param>
        /// <param name="port">The server port.</param>
        public NetServer(IServerNetworkingProvider networkingProvider, NetServerSettings settings, IPAddress address, int port)
        {
            if (networkingProvider == null) throw new ArgumentNullException("networkingProvider");
            if (settings == null) throw new ArgumentNullException("settings");
            if (address == null) throw new ArgumentNullException("address");

            _Timer = new NetTimer();
            RunningTime = _Timer;

            Settings = settings;

            _Clients = new ClientInfoCollection(Settings.MaxConnections, true, true);
            _RecentDisconnections = new List<ClientInfo>(Settings.MaxConnections);

            _LocalEndPoint = new IPEndPoint(address, port);

            _NetProvider = networkingProvider;
            _NetProvider.ServerSettings = settings;
            _NetProvider.ErrorAction = OnError;
            _NetProvider.ReceivePacketAction = ReceivePacket;
            _NetProvider.RunningTime = _Timer;
        }

        /// <summary>
        /// Start the server, this should be called after adding services.
        /// </summary>
        public void Start()
        {
            if (!_Timer.IsRunning)
            {
                _Timer.Reset();
                _Timer.Start();

                _NetProvider.Start(_LocalEndPoint);
                _LastPingTime = _Timer.ElapsedMilliseconds;
            }
        }

        /// <summary>
        /// Stop the server.
        /// </summary>
        public void Stop()
        {
            if (_Timer.IsRunning)
            {
                _Timer.Stop();
                _NetProvider.Disconnect();
                _Clients.Clear();
            }
        }


        /// <summary>
        /// Causes the server to continue processing, this should
        /// be called in a loop as frequently as required.
        /// </summary>
        public void Update()
        {
            // Capture before update time
            _BeforeUpdateTime = _Timer.ElapsedMilliseconds;

            // Raise BeforeUpdate event
            OnBeforeUpdate();

            //
            // Process any new connections
            //
            ProcessNewConnections();

            //
            // Receives any incoming packets and hands them of to the designated services
            //
            _NetProvider.Receive();

            //
            // Sends all queued packets
            //
            Send();

            //
            // Allow the provider to perform any additional work
            //
            _NetProvider.Update();

            //
            // Ping all connections
            //
            Ping();

            //
            // Remove sockets that have disconnected
            //
            RemoveDisconnected();

            // Raise AfterUpdate event
            OnAfterUpdate();

            // Capture after update time
            _AfterUpdateTime = _Timer.ElapsedMilliseconds;
        }

        /// <summary>
        /// Processes new connections and adds them to the Connection list.
        /// </summary>
        private void ProcessNewConnections()
        {
            INetConnection[] connections = _NetProvider.AcceptConnections();
            for (int i = 0; i < connections.Length; i++)
            {
                INetConnection connection = connections[i];

                Debug.Assert(connection.Connected, "Connection cannot be null when processing new connections");

                _Clients.Add(new ClientInfo() { Connection = connection });

                // Send out a packet to all clients informing them that
                // this client has connected
                SendConnectionPacket(connection.Id);

                OnConnected(connection.Id);
   
            }
        }

        /// <summary>
        /// Removes any clients that have disconnected from the client connections list.
        /// </summary>
        private void RemoveDisconnected()
        {
            for (int i = 0; i < _RecentDisconnections.Count; i++)
            {
                ClientInfo client = _RecentDisconnections[i];
                INetConnection conn = client.Connection;

                OnDisconnected(conn.Id);

                _Clients.Remove(client);

                _NetProvider.Disconnect(conn);

                // Send a packet out informing all clients that this client
                // has disconnected
                SendDisconnectionPacket(conn.Id);

            }

            _RecentDisconnections.Clear();
        }

        /// <summary>
        /// Send any cueued packets from all registered services.
        /// </summary>
        private void Send()
        {
            for (int serviceIndex = 0; serviceIndex < Services.Count; serviceIndex++)
            {
                INetService service = Services[serviceIndex];

                Packet packet = null;
                while ((packet = service.CollectPacket()) != null)
                {
                    // Ensure correct service index
                    packet.ServiceId = serviceIndex;

                    //
                    // Send this packet to the specified targets
                    //
                    if (packet.Targets != null)
                    {
                        for (int i = 0; i < packet.Targets.Length; i++)
                        {
                            // TODO: Make sure GetById does not return null here or we could be screwed
                            ClientInfo client = _Clients.GetById(packet.Targets[i]);
                            SendTo(client.Connection, packet);
                        }
                    }
                    //
                    // Send to all
                    //
                    else if (packet.TargetId == 0)
                    {
                        for (int i = 0; i < _Clients.Count; i++)
                        {
                            SendTo(_Clients[i].Connection, packet);
                        }
                    }
                    //
                    // Send to all but the specified target
                    //
                    else if (packet.SendToAllButTarget)
                    {
                        // Skip target, send to the rest
                        for (int i = 0; i < _Clients.Count; i++)
                        {
                            if (packet.TargetId == _Clients[i].Connection.Id) continue;
                            SendTo(_Clients[i].Connection, packet);
                        }
                    }
                    //
                    // Send to just the specified target
                    //
                    else
                    {
                        ClientInfo client = _Clients.GetById(packet.TargetId);
                        if (client != null) SendTo(client.Connection, packet);
                    }
                }
            }
        }

        private void SendTo(INetConnection connection, Packet packet)
        {
            Debug.Assert(connection != null, "SendTo should never be passed a null connection");

            if (!EnsureConnection(connection)) return;

            if ((packet.PacketFlags & PacketFlags.Reliable) != PacketFlags.None)
            {
                packet.SequenceIndex = connection.CreateNextPacketSequenceIndex(); // assign a sequence
                _NetProvider.SendReliable(connection, packet);
            }
            else
            {
                _NetProvider.SendUnreliable(connection, packet);
            }
        }

        /// <summary>
        /// Checks if the given connection is still connected, if not then adds
        /// it to the recent disconnections list
        /// </summary>
        /// <param name="connection">The connection to ensure</param>
        /// <returns>True if still connected, False if disconnected</returns>
        private bool EnsureConnection(INetConnection connection)
        {
            // Do not send to disconnected connections
            if (!connection.Connected)
            {
                ClientInfo client = _Clients.GetByRemoteEndPoint(connection.RemoteEndPoint);
                if (!_RecentDisconnections.Contains(client)) _RecentDisconnections.Add(client);
                return false;
            }

            return true;
        }

        /// <summary>
        /// Gets a currently connected connection by connection id.
        /// </summary>
        /// <param name="connectionId">The id of the connection to get.</param>
        /// <returns>The connection</returns>
        public INetConnection GetConnectionById(int connectionId)
        {
            ClientInfo client = _Clients.GetById(connectionId);
            if (client != null) return client.Connection;
            return null;
        }

        /// <summary>
        /// Gets a currently connected connection by index.
        /// </summary>
        /// <param name="idx">The index of the connection to get</param>
        /// <returns>The connection</returns>
        public INetConnection GetConnectionByIndex(int idx)
        {
            ClientInfo client = _Clients[idx];
            if (client != null && client.Connection.Connected) return client.Connection;
            return null;
        }

        /// <summary>
        /// Send ping packets to connections that have elapsed last seen times greater than the ping frequency.
        /// </summary>
        private void Ping()
        {
            // Disabled
            if (Settings.PingFrequency == 0) return;

            if (ElapsedPingTime > Settings.PingFrequency)
            {
                for (int i = 0; i < _Clients.Count; i++)
                {
                    ClientInfo client = _Clients[i];
                    INetConnection conn = client.Connection;

                    // Do not send to disconnected connections
                    if (!EnsureConnection(conn)) return;

                    long timeNow = _Timer.ElapsedMilliseconds;
                    Packet pingPacket = CreatePingPacket(conn.Id, timeNow, conn.Latency);
                    client.LastPingedAt = timeNow;
                    client.NumUnreceivedPingAcks++;

                    SendTo(conn, pingPacket);

                    // Disconnect this client if they have exceeded the max allowed
                    if (client.NumUnreceivedPingAcks >= Settings.MaxUnreceivedPingAcks)
                    {
                        if (!_RecentDisconnections.Contains(client)) _RecentDisconnections.Add(client);
                    }
                }

                _LastPingTime = _Timer.ElapsedMilliseconds;
            }
        }

        /// <summary>
        /// Processes system packets
        /// </summary>
        protected override void ProcessSystemPacket(Packet packet)
        {
            if (packet.ServiceId == SystemPacketTypes.Ping)
            {
                ProcessPingPacket(packet);
            }
        }

        /// <summary>
        /// Called by the implementation of IServerNetworkingProvider when
        /// a packet is received
        /// </summary>
        private void ReceivePacket(byte[] rawPacket, INetConnection connection)
        {
            // Do not receive from disconnected clients
            EnsureConnection(connection);

            Packet packet = (Packet)rawPacket;

            packet.SenderId = connection.Id;
            DeliverPacketToService(packet);
        }

        /// <summary>
        /// Occurs when a connection encounters a socket error
        /// </summary>
        protected override void OnError(NetErrorEventArgs args)
        {
            base.OnError(args);

            // TODO: This does not disconnect the connection, which is something we should do
            // depending on the type of socket error
            if (args.Connection != null)
            {
                EnsureConnection(args.Connection);
            }
        }

        #region System Packet Helper Methods
        private Packet CreateConnectionPacket(int connectedId)
        {
            _PacketWriter.Reset();
            Packet packet = _PacketWriter.ToPacket(connectedId, 0, SystemPacketTypes.ConnectionBroadcast, PacketFlags.Reliable);
            return packet;
        }

        private Packet CreateDisconnectionPacket(int disconnectedId)
        {
            _PacketWriter.Reset();
            Packet packet = _PacketWriter.ToPacket(disconnectedId, 0, SystemPacketTypes.DisconnectionBroadcast, PacketFlags.Reliable);
            return packet;
        }

        private Packet CreatePingPacket(int targetId, long ticks, int latency)
        {
            _PacketWriter.Reset();
            _PacketWriter.Write(ticks);
            _PacketWriter.Write((ushort)latency);
            Packet packet = _PacketWriter.ToPacket(0, targetId, SystemPacketTypes.Ping, PacketFlags.None);
            return packet;
        }

        private void ProcessPingPacket(Packet packet)
        {
            ClientInfo client = _Clients.GetById(packet.SenderId);
            PacketReader reader = new PacketReader(packet);
            long ticks = reader.ReadInt64();

            // is this the response we are waiting for?
            if (client.LastPingedAt == ticks)
            {
                client.Connection.Latency = (int)(_Timer.ElapsedMilliseconds - ticks);
                client.NumUnreceivedPingAcks = 0;
            }
        }

        /// <summary>
        /// Tell all connections about a new connection
        /// </summary>
        /// <param name="connectedId">The id of the connected client</param>
        private void SendConnectionPacket(int connectedId)
        {
            Packet connectionPacket = CreateConnectionPacket(connectedId);

            for (int i = 0; i < _Clients.Count; i++)
            {
                INetConnection conn = _Clients[i].Connection;

                connectionPacket.TargetId = conn.Id;

                SendTo(conn, connectionPacket);

            }
        }

        /// <summary>
        /// Tell all connections about a new disconnection
        /// </summary>
        /// <param name="disconnectedId"></param>
        private void SendDisconnectionPacket(int disconnectedId)
        {
            Packet disconPacket = CreateDisconnectionPacket(disconnectedId);

            for (int i = 0; i < _Clients.Count; i++)
            {
                INetConnection conn = _Clients[i].Connection;

                disconPacket.TargetId = conn.Id;

                SendTo(conn, disconPacket);
            }
        }
        #endregion

    }
}
